package com.sap.psr.vulas.monitor.touch;

import java.util.HashSet;
import java.util.Set;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.sap.psr.vulas.ConstructId;
import com.sap.psr.vulas.backend.BackendConnectionException;
import com.sap.psr.vulas.backend.BackendConnector;
import com.sap.psr.vulas.core.util.CoreConfiguration;
import com.sap.psr.vulas.java.JavaClassInit;
import com.sap.psr.vulas.java.JavaConstructorId;
import com.sap.psr.vulas.java.JavaId;
import com.sap.psr.vulas.java.JavaMethodId;
import com.sap.psr.vulas.monitor.ClassVisitor;

public class ConstructIdUtil {

	private static Log log = null;
	private static final Log getLog() {
		if(ConstructIdUtil.log==null)
			ConstructIdUtil.log = LogFactory.getLog(ConstructIdUtil.class);
		return ConstructIdUtil.log;
	}
	
	private static ConstructIdUtil instance = null;

	private Set<ConstructId> appConstructs = null;

	private ConstructIdUtil() {
		try {
			final Set<com.sap.psr.vulas.shared.json.model.ConstructId> app_constructs = BackendConnector.getInstance().getAppConstructIds(CoreConfiguration.buildGoalContextFromGlobalConfiguration(), CoreConfiguration.getAppContext());
			this.appConstructs = new HashSet<ConstructId>();
			for(com.sap.psr.vulas.shared.json.model.ConstructId cid: app_constructs) {
				this.appConstructs.add(JavaId.toCoreType(cid));
			}
		} catch (ConfigurationException e) {
			ConstructIdUtil.getLog().error(e.getMessage(), e);
		} catch (BackendConnectionException e) {
			ConstructIdUtil.getLog().error(e.getMessage(), e);
		}
	}

	public synchronized static ConstructIdUtil getInstance() {
		if(ConstructIdUtil.instance==null)
			ConstructIdUtil.instance =  new ConstructIdUtil();
		return ConstructIdUtil.instance;
	}

	/**
	 * Checks whether the given {@link ConstructId} is part of the application under analysis.
	 * The check is implemented by looing at the definition context, which should be either
	 * class or enum. The reason is that, e.g., static initializers are not considered at
	 * the time of the source code analysis, hence, are not part of the collection.
	 * @param _jid
	 * @return
	 */
	public boolean isAppConstruct(ConstructId _jid) {
		boolean is_app_construct = false;

		// We only instrument clinit, methods and constructors
		if(ConstructIdUtil.isOfInstrumentableType(_jid)) {
			final ConstructId context = _jid.getDefinitionContext();
			is_app_construct = this.appConstructs!=null && this.appConstructs.contains(context);
		}
		else {
			ConstructIdUtil.getLog().error("Expected <CLINIT>, method or constructor, got [" + _jid.toString() + "]");
		}

		return is_app_construct;
	}

	/**
	 * Returns true if the given {@link ConstructId} is neither part of the application nor a test method, false otherwise.
	 * @param _jid
	 * @return
	 */
	public boolean isLibConstruct(ConstructId _jid) {
		boolean is_lib_construct = false;

		// We only instrument clinit, methods and constructors
		if(ConstructIdUtil.isOfInstrumentableType(_jid)) {
			final ConstructId context = _jid.getDefinitionContext();
			is_lib_construct = this.appConstructs!=null && !this.appConstructs.contains(context);

			// Not part of the app, now check that is not a JUnit test method of the app
			if(is_lib_construct) {
				if(_jid instanceof JavaMethodId) {
					is_lib_construct = !((JavaMethodId)_jid).isTestMethod();
				}
			}
		}
		else {
			ConstructIdUtil.getLog().error("Expected <CLINIT>, method or constructor, got [" + _jid.toString() + "]");
		}

		return is_lib_construct;
	}

	/**
	 * Returns true if the given {@link JavaId} is an instance of {@link JavaClassInit}, {@link JavaMethodId} or
	 * {@link JavaConstructorId}, false otherwise. 
	 * @param _jid
	 * @return
	 */
	public static boolean isOfInstrumentableType(ConstructId _jid) {
		return _jid instanceof JavaClassInit || _jid instanceof JavaConstructorId || _jid instanceof JavaMethodId;
	}

	/**
	 * Given a qualified name return the ConstructId that represent it. For now is
	 * implemented only on constructors and method (also including <clinit> and <init>).
	 * If the type requested is null or is not in the range of teh allowed types the method return null
	 * 
	 * @param _qname the qname of the construct. 
	 * @param type can be CONSTRUCTOR,CLASSINIT,METHOD,CLASS,NESTED_CLASS
	 * @return Return the java representation of this constructid or null if is not found
	 */
	public ConstructId getConstructidFromQName(String _qname, String type){
		if(_qname == null || type==null) return null;
		// Constructor
		if(_qname.contains("<init>") || type.contains("CONSTRUCTOR")) {
			_qname = ClassVisitor.removeParameterQualification(_qname);
			return JavaId.parseConstructorQName(_qname);
		}
		// Static initializer
		else if(_qname.contains("<clinit>") || type.contains("CLASSINIT")) {
			return JavaId.parseClassInitQName(_qname.substring(0, _qname.lastIndexOf('>')+1));
		}
		// Method
		else if(type.contains("METHOD")){
			// Cleanup the method's string representation for parsing
			_qname = ClassVisitor.removeParameterQualification(_qname);
			return JavaId.parseMethodQName(_qname);
		}
		else if(type.contains("CLASS") || type.contains("NESTED_CLASS")){
			return JavaId.parseClassQName(_qname);
		}
		else if(type.contains("ENUM")){
			return JavaId.parseEnumQName(_qname);
		}
		else return null;
	}
}
